Prop Delegation
In the Banner
example from the Spectrum of Components lesson, we saw how our LoggedIn
banner had to “forward” some props along:
function LoggedInBanner({ user, // These two props: type, children,}) { if ( !user || user.registrationStatus === 'unverified' ) { return null; }
// ...are forwarded along to Banner: return <Banner type={type}>{children}</Banner>;}
What if this component had 10 forwarded props instead of 2? Would we have to list them all out, one by one?
Fortunately, React won't make us do that. Instead, we can take advantage of rest parameters and spread syntax 👀.
Here's what it looks like:
function LoggedInBanner({ user, // Collect all unspecified props: ...delegated}) { if ( !user || user.registrationStatus === 'unverified' ) { return null; }
// And pass them onto Banner: return <Banner {...delegated} />}
I've chosen the name delegated
because I feel like it's semantically appropriate, but we can name this variable whatever we want. Some folks prefer rest
:
function LoggedInBanner({ user, ...rest}) { if ( !user || user.registrationStatus === 'unverified' ) { return null; }
return <Banner {...rest} />}
For consistency, I'll use delegated
throughout this course.
If we were to console.log
the delegated
variable, we'd see an object containing all of the other props provided to this component. For example:
console.log(delegated);/* { type: 'success', children: 'Account registered!', }*/
To apply these props onto our Banner
element, we create an expression slot with curly brackets, and spread the props along using spread syntax (...
):
// This code:<Banner {...delegated} />
// ...is equivalent to this code:<Banner type={delegated.type} children={delegated.children}/>
// ...which is the same thing as this code:<Banner type={delegated.type}> {delegated.children}</Banner>
To go one step further in demystifying this new syntax, here's how it gets transpiled to plain JavaScript:
// This JSX...<UserProfileCard user={currentUser} {...delegated} />
// ...turns into this JavaScript:React.createElement( UserProfileCard, { user: currentUser, ...delegated });
Supercharged HTML tags
Video Summary
Some of the React components we build are essentially "supercharged wrappers" around built-in HTML tags. For example, let's consider the TextInput
component we saw in the last module:
function TextInput({ id, label, type, value, onChange }) { const generatedId = React.useId(); const appliedId = id || generatedId;
return ( <div className="text-input"> <label htmlFor={appliedId}> {label} </label> <input id={appliedId} type={type} value={value} onChange={onChange} /> </div> );}
From the perspective of the consumer, we tend to think of this <TextInput>
component as “an <input>
tag with some bells and whistles”. The bells and whistles are the fact that this component will generate an ID, and apply it if needed, so that the label is wired up and we have good usability + accessibility.
For example, we might use it in a login form, in the same way we'd use an <input type="email">
and <input type="password">
:
import TextInput from './TextInput';
function LoginForm() { const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState('');
function handleLogin() { alert(`Logged in with ${email}`); }
return ( <form onSubmit={handleLogin}> {/* Use the TextInput component the same way we'd use an <input type="email">: */} <TextInput label="Email" type="email" value={email} onChange={(event) => { setEmail(event.target.value); }} /> <TextInput label="Password" type="password" value={password} onChange={(event) => { setPassword(event.target.value); }} /> <button> Submit </button> </form> );}
There's a problem, though: Standard <input>
elements support a wide set of attributes, like HTML validation or data attributes.
<TextInput // Adding 2 new attributes: required={true} data-test-id="login-email-field"
label="Email" type="email" value={email} onChange={(event) => { setEmail(event.target.value); }}/>
We might expect these attributes to work, but they don't; we haven't explicitly added them as props!
Remember, React has no idea that we have this semantic link between the TextInput
component and the <input>
element rendered within. All React knows is that we've defined a component, and that component returns a chunk of JSX.
We could explicitly add a bunch of valid attributes as props to our TextInput
component:
function TextInput({ id, label, type, value, onChange, required, minLength, maxLength, pattern,}) { // Content removed, for brevity}
But what about data attributes? There are an infinite number of them! We can't possibly predict all valid values.
Fortunately, we can use prop delegation to solve this problem for us.
Here's what it looks like:
import React from 'react';
function TextInput({ id, label, ...delegated }) { const generatedId = React.useId(); const appliedId = id || generatedId;
return ( <div className="text-input"> <label htmlFor={appliedId}>{label}</label> <input id={appliedId} {...delegated} /> </div> );}
export default TextInput;
We collect all of the props not explicitly used by TextInput
into a delegated
object. Then, we spread those props onto the <input>
.
Now, any attribute we add to <TextInput>
elements will automatically be forwarded to this input. We can now officially treat this component as a supercharged <input>
!
The cool thing about this pattern is that we can spread the props onto any element we want, it isn't limited to the very top-level HTML element returned.
This video revisits the TextInput
component we created in Module 3, when learning about the Rules of Hooks
Here's the sandbox from the video:
Code Playground